import * as fs from 'fs';
import * as path from 'path';
import archiver, { Archiver, ArchiverError, ProgressData } from 'archiver';

import { walk, WalkStats } from 'walk';
import { FilePathFilter } from './filePathFilter';
export interface ZipResult {
  fileCount: number;
}
export class Zipper {
  private archiver!: Archiver;

  private srcDir = '';

  private totalAddedFiles = 0;

  constructor(
    private readonly log: any,
    private readonly filenameFiltersAnd: FilePathFilter[] = [],
    private readonly filenameFiltersOr: FilePathFilter[] = []) {
  }

  private addSingleFileToZip(srcFile: string) {
    const index: number = srcFile.lastIndexOf(path.sep);
    const fileName: string = srcFile.substring(index + 1);
    if (this.filenameFiltersAnd.every(filter => filter.includes(fileName)) || this.filenameFiltersOr.some(filter => filter.includes(fileName))) {
      this.log.debug(` Add: ${srcFile}`);
      this.archiver.file(srcFile, {
        name: fileName,
        prefix: ''
      });
    } else {
      this.log.debug(`Skip: ${srcFile}`);
    }
  }

  zipDirectory(srcDir: string, targetPath: string, fingerprintsFile?: string): Promise<ZipResult> {
    this.srcDir = srcDir;
    this.totalAddedFiles = 0;

    return new Promise<ZipResult>((resolve, reject) => {
      this.archiver = this.createArchiver(reject);
      const zipOutput = this.createOutputStream(targetPath, resolve);
      this.archiver.pipe(zipOutput);

      if (fingerprintsFile) {
        this.addSingleFileToZip(fingerprintsFile);
      }

      if (fs.lstatSync(srcDir).isDirectory()) {
        this.log.debug('Discovering files in source directory.');
        // followLinks is set to true to conform to Common Client behavior.
        const walker = walk(this.srcDir, { followLinks: true });
        walker.on('file', this.addFileToArchive);
        walker.on('end', () => {
          this.log.debug('Finished discovering files in source directory.');
          this.archiver.finalize();
        });
      } else {
        this.addSingleFileToZip(srcDir);
        this.archiver.finalize();
      }
    });
  }

  private createArchiver(reject: any) {
    const result = archiver('zip', { zlib: { level: 9 } });

    result.on('warning', (err: ArchiverError) => {
      this.log.warn(`Archiver: ${err.message}`);
    });

    result.on('error', (err: ArchiverError) => {
      reject(err);
    });

    result.on('progress', (data: ProgressData) => {
      this.totalAddedFiles = data.entries.processed;
    });
    return result;
  }

  private createOutputStream(targetPath: string, resolve: (value: ZipResult) => void) {
    const result = fs.createWriteStream(targetPath);
    result.on('close', () => {
      const zipResult: ZipResult = {
        fileCount: this.totalAddedFiles
      };

      this.log.info(`Acrhive creation completed. Total bytes written: ${this.archiver.pointer()}, files: ${this.totalAddedFiles}.`);
      resolve(zipResult);
    });
    return result;
  }

  private addFileToArchive = (parentDir: string, fileStats: WalkStats, discoverNextFile: () => void) => {
    const absoluteFilePath = path.resolve(parentDir, fileStats.name);
    const relativeFilePath = path.relative(this.srcDir, absoluteFilePath);

    // relativeFilePath is normalized to contain forward slashes independent of the current OS. Examples:
    //      page.cs                             - if page.cs is at the project's root dir
    //      services/internal/myservice.js      - if myservice.js is in a nested dir
    if (this.filenameFiltersAnd.every(filter => filter.includes(relativeFilePath)) && (!this.filenameFiltersOr.length || this.filenameFiltersOr.some(filter => filter.includes(relativeFilePath)))) {
      this.log.debug(` Add: ${absoluteFilePath}`);

      const relativeDirInArchive = path.relative(this.srcDir, parentDir);
      this.archiver.file(absoluteFilePath, {
        name: fileStats.name,
        prefix: relativeDirInArchive
      });
    } else {
      this.log.debug(`Skip: ${absoluteFilePath}`);
    }

    discoverNextFile();
  };
}